/**
 * PSWrap.js is built on top of Prototype and Scriptaculous. It aims to provide wrappers to simplify
 * the use of the scriptaculous queueing system and improve syntax, semantics, and general ease of use.
 * 
 * @author Peter Swan
 * @copyright 2008
 * @license MIT
 */

/* TODO: 
 * add a preventIf callback which prevents an event from firing if it returns true/false
 * determine how to leverage position: with-last to provide multiple effects on single element
 * 
 * add default open, close, move functionality to every animatable element
 * 
 * must do bind as event listener and stop event if it is present
 *  
 * can probably make a collection without parallel by using a single queue for all events on objects.
 * could NOT set a limit on the queue and would have to use with-last.
 * 
 * use identify instead of readAttribute('id') to get around no id provided errors!
 * 
 * modify _action_open/_action_close/etc. to allow for event specification as well; e.g. elmid_action_open_click
 * (id of element)_(type)_(function)_(event)
 * 
 * / 

/**
 * @namespace contains all library functionality
 */
var PSWrap = 
{
	/** Version of the Library 
	 * @type String
	 */
	Version: '0.1.0',
	/** Required version of Scriptaculous 
	 * @type String
	 */
	REQUIRED_SCRIPTACULOUS: '1.8.0',
	/**
	 * Checks dependencies of library
	 * @return {Void}
	 */
	load: function(){
		function convertVersionString(versionString){
			var r = versionString.split('.');
			return parseInt(r[0]) * 100000 + parseInt(r[1]) * 1000 + parseInt(r[2]);
		}
		
		if ((typeof Scriptaculous == 'undefined') ||
			(convertVersionString(Scriptaculous.Version) <
			convertVersionString(PSWrap.REQUIRED_SCRIPTACULOUS)) ||
			typeof(Effect) == 'undefined' ) //|| typeof(Builder) == 'undefined'	) 
			throw ("PSWrap requires the Scriptaculous JavaScript framework >= " +
			PSWrap.REQUIRED_SCRIPTACULOUS + ' with Effects and Builder');
	}
};
PSWrap.load();

/* TODO: make this deep copy arrays as well */
Object.extend( Object, {
	recursiveExtend: function( destination, source){
		for( var k in source){
			if( typeof( destination[k]) == 'object' && typeof( source[k]) == 'object'){
				Object.recursiveExtend( destination[k], source[k]);
			}else{
				destination[k] = source[k];
			}
		}
		return destination;
	}
});

Element.addMethods({
		selectChildren: function( element, c){
			return $(element).childElements().findAll( function( elm ){
					return elm.match(c);
			});
		}
});

/* modify Effect.Base and Effect.ScopedQueue to only execute beforeStart callbacks
 * if effect was successfully added to the queue.
 */
Effect.Base.addMethods(
{
	start: function(options) {
	    function codeForEvent(options,eventName){
	      return (
	        (options[eventName+'Internal'] ? 'this.options.'+eventName+'Internal(this);' : '') +
	        (options[eventName] ? 'this.options.'+eventName+'(this);' : '')
	      );
	    }
	    if (options && options.transition === false) options.transition = Effect.Transitions.linear;
	    this.options      = Object.extend(Object.extend({ },Effect.DefaultOptions), options || { });
	    this.currentFrame = 0;
	    this.state        = 'idle';
	    this.startOn      = this.options.delay*1000;
	    this.finishOn     = this.startOn+(this.options.duration*1000);
	    this.fromToDelta  = this.options.to-this.options.from;
	    this.totalTime    = this.finishOn-this.startOn;
	    this.totalFrames  = this.options.fps*this.options.duration;
	    
		this.render = (function() {
	      function dispatch(effect, eventName) {
	        if (effect.options[eventName + 'Internal'])
	          effect.options[eventName + 'Internal'](effect);
	        if (effect.options[eventName])
	          effect.options[eventName](effect);
	      }
	 
	      return function(pos) {
	        if (this.state === "idle") {
	          this.state = "running";
	          dispatch(this, 'beforeSetup');
	          if (this.setup) this.setup();
	          dispatch(this, 'afterSetup');
	        }
	        if (this.state === "running") {
	          pos = (this.options.transition(pos) * this.fromToDelta) + this.options.from;
	          this.position = pos;
	          dispatch(this, 'beforeUpdate');
	          if (this.update) this.update(pos);
	          dispatch(this, 'afterUpdate');
	        }
	      }
	    })();
	    
		if (!this.options.sync) {
			// if the effect was successfully added to the queue, execute beforeStart
			if( Effect.Queues.get(Object.isString(this.options.queue) ? 'global' : this.options.queue.scope).add(this))
				this.event('beforeStart');
		}
		// if this is a synched event it's definitely happening, so execute beforeStart
		else{
			this.event('beforeStart');
		}
  }
});

Effect.ScopedQueue.addMethods(
{
	add: function(effect){
		var timestamp = new Date().getTime();
		
		var position = Object.isString(effect.options.queue) ? effect.options.queue : effect.options.queue.position;
		
		switch (position) {
			case 'front':
				// move unstarted effects after this effect  
				this.effects.findAll(function(e){
					return e.state == 'idle'
				}).each(function(e){
					e.startOn += effect.finishOn;
					e.finishOn += effect.finishOn;
				});
				break;
			case 'with-last':
				timestamp = this.effects.pluck('startOn').max() || timestamp;
				break;
			case 'end':
				// start effect after last queued effect has finished
				timestamp = this.effects.pluck('finishOn').max() || timestamp;
				break;
		}
		
		effect.startOn += timestamp;
		effect.finishOn += timestamp;
		
		/* return true if effect was inserted into the queue false otherwise */
		var ret = false;
		if (!effect.options.queue.limit || (this.effects.length < effect.options.queue.limit)){
			ret = true;
			this.effects.push(effect);
		}
		
		if (!this.interval) 
			this.interval = setInterval(this.loop.bind(this), 15);
		
		return ret;
	}
});

/* patch BlindUp and BlindDown for IE */
Effect.BlindUp = function(element) {
  element = $(element);
  element.makeClipping();
  return new Effect.Scale(element, 0,
    Object.extend({ scaleContent: false, 
      scaleX: false, 
      restoreAfterFinish: true,
      /* Change here: IE will not correctly clip if there are layers inside element! */
      afterSetupInternal: function( effect ){
	      effect.element.makePositioned().makeClipping();
      },
      afterFinishInternal: function(effect) {
        effect.element.hide().undoClipping().undoPositioned();
      } 
    }, arguments[1] || { })
  );
};
Effect.BlindDown = function(element) {
  element = $(element);
  var elementDimensions = element.getDimensions();
  return new Effect.Scale(element, 100, Object.extend({ 
    scaleContent: false, 
    scaleX: false,
    scaleFrom: 0,
    scaleMode: {originalHeight: elementDimensions.height, originalWidth: elementDimensions.width},
    restoreAfterFinish: true,
    afterSetupInternal: function(effect) {
      /* Change here: IE will not correctly clip if there are layers inside the element!
      effect.element.makeClipping().setStyle({height: '0px'}).show();*/
      effect.element.makePositioned().makeClipping().setStyle({height: '0px'}).show();
    },  
    afterFinishInternal: function(effect) {
      /*effect.element.undoClipping();*/
      effect.element.undoClipping().undoPositioned();
    }
  }, arguments[1] || { }));
};

Effect.Appear = function(element) {
  element = $(element);
  var options = Object.extend({
  from: (element.getStyle('display') == 'none' ? 0.0 : element.getOpacity() || 0.0),
  to:   1.0,
  // force Safari to render floated elements properly
  afterFinishInternal: function(effect) {
    effect.element.forceRerendering();
  },
  beforeSetupInternal: function(effect) {
    effect.element.setOpacity(effect.options.from).show(); 
  }}, arguments[1] || { });
  return new Effect.Opacity(element,options);
};

Effect.Scroll = Class.create( Effect.Base, {
	initialize: function( element ){
		this.element = $(element);
    	if (!this.element) throw(Effect._elementDoesNotExistError);
		var options = Object.extend({
			x: 0,
			y: 0,
			mode: 'absolute'
		}, arguments[1] || {});
		this.start(options);
	},
	setup: function(){
		if( this.element === window){
			if( !Object.isUndefined(this.element.scrollY)){
				this.originalTop = this.element.scrollY;
				this.originalLeft = this.element.scrollX;
				//console.log('scrollY/X: ' + this.originalTop + ', ' + this.originalLeft);
			}else if( !Object.isUndefined(document.documentElement.scrollTop) ){
				this.originalTop = document.documentElement.scrollTop;
				this.originalLeft = document.documentElement.scrollLeft;
				//console.log('documentElement.scrollTop/Left: ' + this.originalTop + ', ' + this.originalLeft);
			}else if( !Object.isUndefined(document.scrollTop) ){
				this.originalTop = document.scrollTop;
				this.originalLeft = document.scrollLeft;
				//console.log('scrollTop/Left: ' + this.originalTop + ', ' + this.originalLeft);
			}else{
				this.orginalTop = this.originalLeft = 0;
			}
		}else{
			this.originalTop = this.element.scrollTop;
			this.originalLeft = this.element.scrollLeft;
		}
		
		if( this.options.mode == 'absolute'){
			this.options.x -= this.originalLeft;
			this.options.y -= this.originalTop;
		}
	},
	update: function( pos ){
		var new_x = this.originalLeft + this.options.x * pos;
		var new_y = this.originalTop + this.options.y * pos;
		this._setScroll( new_x, new_y);
	},
	_setScroll: function( x, y){
		if( this.element === window){
			window.scrollTo(x, y);
		}else{
			this.element.scrollTop = x;
			this.element.scrollLeft = y;
		}
	}
});

/**
 * @namespace contains all debug functionality
 */
PSWrap.Debug = 
{
	debug: false,
	/** toggle debugging 
	 * @param {Boolean} d
	 */
	setDebug: function( d ){
		if( d ){
			this.debug = true;
		}else{
			this.debug = false;
		}
	},
	/**
	 * print to the console if it is defined
	 * @param {String} msg
	 */
	printToConsole: function(msg, doit){
		if( PSWrap.Debug.debug || doit ){
			try{
				console.log(msg);
			}catch(e){}
		}
	},
	
	printObjectToConsole: function(obj, spaces){
		if (PSWrap.Debug.debug) {
			spaces = spaces || '';
			for (k in obj) {
				if (typeof(obj[k]) == 'object') {
									
				}
				else {
					
				}
			}
		}
	},
	/**
	 * print to alert box
	 * @param {String} msg
	 */
	printAlert: function(msg){
		if (PSWrap.Debug.debug) {
			alert(msg);
		}
	}
};

/**
 * @namespace Contains utility functions
 */
PSWrap.Util = {
	/**
	 * Does deep copies of objects to create a new combined object.  Properties of each object in the array
	 * will overwrite the properties of the previous object.
	 * 
	 * @param {Mixed} options... one or more objects containing options
	 * @return {Object} An object containing the 
	 */
	assembleOptions: function(){
		var options = new Object();
		$A(arguments).each(function(o){
			Object.recursiveExtend(options, o);
		});
		return options;
	},
	
	removeOptions: function(obj, arr){
		$A(arr).each( function(e){
			if(obj[e]){
				obj[e] = null;
				delete obj[e];	
			}
		});
	},
	
	deepCopy: function( dest, source){
		for( k in source){
			if( typeof(source[k]) == 'object'){
				dest[k] = new Object();
				deepCopy( dest[k], source[k]);
			}else if( typeof(source[k]) == 'array'){
				dest[k] = $A(source[k]).clone();
			}else{
				dest[k]	= source[k];
			}
		}
	}
};

/**
 * @namespace Contains error/exception handling functions
 */
PSWrap.Error = 
{
	/**
	 * @param {String} type
	 * @param {String} required_type
	 */
	TypeError: function(type, required_type){
		throw { name: 'PSWrap.TypeError', message: type + ' found where ' + required_type + ' required.'};
	},
	
	/**
	 * @param {Mixed} id
	 */
	ElementNotFoundError: function(){
		throw { name: 'PSWrap.ElementNotFoundError', message: 'element not found'};
	},
	
	/**
	 * @param {Mixed} elm
	 */
	ScopeWarning: function( elm ){
		throw { name: 'PSWrap.ScopeWarning', message: 'no scope was provided for ' + elm + ', this may result in unexpected behavior'};
	}
};

PSWrap.State = {
	state_string: '',
	objects: {},
	anchor: null,
	recordState: function(obj){
		
		/* probably want to remove state from state string if state is '' 
		 * implemented in old version on vanalen.org
		 */
		PSWrap.State.addObject(obj);
		var r = new RegExp(obj.getId() + '=[^&]+');
		var s = obj.getState();
		
		if( r.test(PSWrap.State.state_string)){
			
			PSWrap.State.state_string = PSWrap.State.state_string.replace(r, s);
		}else{
			if( PSWrap.State.state_string != ''){
				PSWrap.State.state_string += '&';
			}
			PSWrap.State.state_string += s;
		}
	},
	
	addObject: function(obj){
		if( Object.isUndefined(PSWrap.State.objects[obj.getId()]) ){
			PSWrap.State.objects[obj.getId()] = obj;
		}
	},
	
	getStateString: function(){
		return PSWrap.State.state_string;
	},
	
	applyState: function(){
		var s = window.location.hash;
		
		if(s){
			s = s.split('#')[1];
			
			var a = s.split('&');
			if (Object.isArray(a)) {
				a.each(function(e){
					e = e.split('=');
					if (!Object.isUndefined(PSWrap.State.objects[e[0]])) {
						
						PSWrap.State.objects[e[0]].setState(e[1]);
					}
				});
				//PSWrap.State.state_string = s;
			}
		}
	},
	
	getUrl: function(){
		var href = window.location.href.split('?')[0].split('#')[0];
		var query = window.location.search;
		/* want to append to query instead of using hash (like google maps) */
		
		return href + query + this.getHash();
	},
	
	getHash: function(){
		return  '#' + PSWrap.State.getStateString();
	}
}

/**
 * @namespace Contains functions and classes for easily animating elements
 */
PSWrap.Animation = {
	HOOKS: ['beforeStart', 'afterFinish', 'beforeSetup', 'afterSetup', 'beforeUpdate', 'afterUpdate'],
	PARALLEL: 1,
	SEQUENTIAL: 2
};

PSWrap.Animation.Elements = {
	_elements: $H(),
	addElement: function(elm){
		var id = elm.getElement().readAttribute('id');
		if( id ){
			PSWrap.Animation.Elements._elements.set(id, elm);
		}
	},
	getElement: function(id){
		if( id ){
			return PSWrap.Animation.Elements._elements.get(id);
		}
	}
}

PSWrap.DynamicLoader = Class.create({
	initialize: function(element, options){
		options = options || {};
		this._after = null;
		this._before = null;
		this._parent = null;
		this._first = this;
		this._loaded = false;
		this._loading = false;
		this._element_list = $A();
		this._attempts = 0;
		this._max_attempts = options['max_attempts'] || 3;
		this._attempt_interval = options['attempt_interval'] || 0.25;
		options = options || {};
		
		this._containing_element = $(element);
		if( this._containing_element ){
			
			var content_path = this._containing_element.down('a');
			if( content_path && content_path.hasClassName('dynamic_content_path')){
				
				this._url = content_path.readAttribute('href');
				if( this._url){
					
					var beforeLoad = this._containing_element.down('.beforeLoad');
					if( beforeLoad ){
						beforeLoad = eval( beforeLoad.innerHTML.stripTags().stripScripts());
					}
					var afterLoad = this._containing_element.down('.afterLoad');
					if( afterLoad ){
						afterLoad = eval( afterLoad.innerHTML.stripTags().stripScripts());
					}
					
					this._options = {
						beforeLoad: Object.isFunction(options['beforeLoad'])?options['beforeLoad']:Object.isFunction(beforeLoad)?beforeLoad:Prototype.emptyFunction,
						afterLoad: Object.isFunction(options['afterLoad'])?options['afterLoad']:Object.isFunction(afterLoad)?afterLoad:Prototype.emptyFunction
					}
					
					this._element_list.push(this._containing_element);
					var dependencies = this._containing_element.select('ul.dependencies > li');
					
					this._containing_element.select('ul.dependencies > li').each( function(e){
						if( e.innerHTML ){
							var elm = $( e.innerHTML.stripScripts().stripTags().strip() );
							if( elm ){
								var after = 0;
								this._addElement(elm);
								if( e.hasClassName('after') ){
									after = 1;
								}
								this._addDependency(new PSWrap.DynamicLoader(elm), after);
							}else{
								
							}
						}else{
							
						}
					}, this);
					return true;
				}else{
					return false;
				}
			}else{
				return false;
			}
		}else{
			return false;
		}
	},
	
	getElements: function(){
		return this._element_list;
	},
	
	_addElement: function(elm){
		this._element_list.push(elm);
	},
	
	_addDependency: function( dl, after){
		if( after ){
			
			if( !this._after ){
				this._setParent(dl);
			}else{
				this._after._setParent(dl);
			}
			this._after = dl;
		}else{
			
			if( !this._before){
				this._first = dl;
			}else{
				this._before._setParent(dl);
			}
			dl._setParent(this);
			this._before = dl;
		}
	},
	
	_setParent: function(dl){
		this._parent = dl;
	},
	
	load: function(){
		if( this._loaded == false && this._loading == false){
			this._loading = true;
			this._first._loadSelf();
		}
	},
	
	_loadSelf: function(){
		
		this._options.beforeLoad(this);
		new Ajax.Request( this._url, {
			method: 'get',
			onSuccess: this._afterLoad.bind(this),
			onFailure: this._onFailure.bind(this)
		});
	},
	
	_loadParent: function(){
		if( this._parent ){
			this._parent._loadSelf();
		}
	},
	
	_afterLoad: function(r){
		this._loading = false;
		this._loaded = true;
		this._containing_element.update(r.responseText);
		this._options.afterLoad.defer(this);
		this._loadParent();
	},
	
	_onFailure: function(r){
		this._loading = false;
		
		if( r.status == 503 ){
			this._attempts++;
			if( this._attempts < this._max_attempts){
				this._loadSelf.bind(this).delay(this._attempt_interval);
			}else{
				
				alert('The application is currently unable to process your request, please try again later.');
			}
		}else{
			alert("Sorry! We can't find the data you requested.");
		}
	}
});

PSWrap.DynamicLoader.Util = {
	afterLoadFuncs: $A(),
	beforeLoadFuncs: $A(),
	isLoadable: function(elm){
		elm = $(elm);
		if( elm ){
			var link = elm.down('a');
			if( link && link.hasClassName('dynamic_content_path')){
				var url = link.readAttribute('href');
				if( url ){
					return true;
				}
			}
		}
		return false;
	},
	addBeforeLoad: function( f ){
		if( Object.isFunction(f)){
			PSWrap.DynamicLoader.Util.beforeLoadFuncs.push(f);
		}
	},
	addAfterLoad: function(f){
		if( Object.isFunction(f) ){
			PSWrap.DynamicLoader.Util.afterLoadFuncs.push(f);
		}
	},
	beforeLoad: function( dl ){
		PSWrap.DynamicLoader.Util.onEvent(PSWrap.DynamicLoader.Util.afterLoadFuncs, dl);
	},
	afterLoad: function( dl ){
		PSWrap.DynamicLoader.Util.onEvent(PSWrap.DynamicLoader.Util.afterLoadFuncs, dl);
	},
	onEvent: function( fl, dl ){
		var l = fl.length;
		for( var i=0; i<l; i++){
			fl[i](dl);
		}
	}
}

PSWrap.Animation.Classnames =
{
	types: {
		'action': {
			'selector': new Template('[class*=#{id}_action:#{option}]'),
			'regexp': new Template('#{id}_action:#{option}(?:_event:([^\\s]+))?')
		},
		'display': {
			'selector': new Template('[class*=#{id}_display:#{option}]'),
			'regexp': new Template('#{id}_display:#{option}')
		}
	},
	getElements: function( elm, type, options){
		var id = elm.identify();
		var ret = {};
		var mytype = null;
		var remove_class = (arguments.length==4?arguments[3]:false);
		if( id && type && (mytype = PSWrap.Animation.Classnames.types[type]) && Object.isArray(options) && options.length){
			options.each( function(o){
				var selector = mytype.selector.evaluate({'id': id, 'option': o});
				ret[o] = $$(selector) || [];
				if( remove_class && ret[o].length ){
					var reg = PSWrap.Animation.Classnames.getRegExp( type, {'id': id, 'option': o});
					ret[o].each( function(e){
						var matches = reg.exec(e.className);
						if( matches ){
							e.removeClassName(matches[0]);
						}
					});
				}
			});	
		}
		return ret;
	},
	getRegExp: function( type, data){
		if( !PSWrap.Animation.Classnames.types[type]) return null;
		return new RegExp(PSWrap.Animation.Classnames.types[type].regexp.evaluate(data));
	},
	getSelector: function( type, data){
		if( !PSWrap.Animation.Classnames.types[type]) return null;
		return PSWrap.Animation.Classnames.types[type].selector.evaluate(data);
	}
};

PSWrap.Animation.Binding = 
{
	bindActionElements: function(obj, bind_method, options, possible, requested){
		var elms_all = null;
		requested = requested || [];
		options = options || {};
		if( requested.length ){
			requested = requested.select(possible.member.bind(possible));
		}else{
			requested = possible;
		}
		if( requested.length ){
			var id = obj.identify();
			elms_all = PSWrap.Animation.Classnames.getElements( obj, 'action', requested);
			requested.each(function(r){
				var elms = elms_all[r];
				var reg = PSWrap.Animation.Classnames.getRegExp( 'action', {'id': id, 'option': r});
				if (elms && elms.length) {
					elms.each(function(e){
						var matches = reg.exec(e.className);
						if (matches ) {
							if( matches[1]){
								//this._bindElement(a, e, matches[1], options);
								obj[bind_method].call(obj, r, e, matches[1], options);
							}else{
								obj[bind_method].call(obj, r, e, 'click', options);
							}
							e.removeClassName(matches[0]);
						}
					}, this);
				}
			}, this);	
		}
		return elms_all;	
	}
}

/**
 * @namespace Contains transitions which supplement Effect.Transitions
 */
PSWrap.Animation.Transitions = 
{
	none: function( pos ){
		return Math.floor(pos);
	}
};

PSWrap.Animation.Directions = {
	FORWARD: 1,
	BACKWARD: -1,
	HORIZONTAL: 1,
	VERTICAL: 2
};

PSWrap.Animation.AnimatableElement = Class.create(
{
	/**
	 * @param {Mixed} element string or DOM element
	 * @param {Object} types types of events to add at creation
	 * @param {Object} options
	 */
	initialize: function( element, options, types){
		/* get the element */
		this._element = $(element);
		if( this._element){
			this._animations = $H();
			this._callbacks = $H();
			options = options || {};
			
			/* set default transition, queue, and before after functions */
			this._options = new Object({
				transition: Effect.Transitions.linear,
				queue: {
					scope: this._element.identify(),
					limit: 0,
					position: 'end'
				}
			});
			Object.recursiveExtend( this._options, options);
			if( !this.getOption('queue').scope){
				PSWrap.Error.ScopeWarning(this._element);
				this.setOptions({queue: {scope: 'PSWrap_animatable_element'}});
			}
			
			this._processCallbacks(this._options);
			
			types = $H(types || {});
			types.each( function( p){
				this.registerAnimationType( p.key, p.value);
			}, this);
			PSWrap.Animation.Elements.addElement(this);
			this.bindElementsByClass();
		}else{
			PSWrap.Error.ElementNotFoundError(element);
		}
	},
	
	identify: function(){
		return this._element.identify();
	},
	
	/**
	 * Returns the element the element on which the AnimatableElement was created 
	 * @return DOM Element
	 */
	getElement: function(){
		return this._element;
	},
	
	/**
	 * Returns the value of the option specified by option.  If context is specified
	 * returns the option's value for the animation specified by context
	 * @param {String} option
	 * @param {String} [context]
	 */
	getOption: function(option, context){
		return this._getOptionsFromContext(context)[option];
	},
	
	/**
	 * @param {String} option the option to set
	 * @param {Mixed} value the value of the option
	 * @param {string} [context] optional context, defaul is global
	 */
	setOption: function(option, value, context){
		var o = this._getOptionsFromContext(context);
		o[option] = value;
	},
	
	/** @ignore */
	_getOptionsFromContext: function(context){
		var a = this._getAnimationType(context);
		if( a){
			return a.options;
		}else{
			return this._options;
		}
	},
	
	/**
	 * @param {Object} options the options to set
	 * @param {String} [context] optional context
	 */
	setOptions: function(options, context){
		Object.recursiveExtend(this._getOptionsFromContext(context), options);
	},
	
	/**
	 * Animates using the effect bound to 'name'
	 * @param {String} name
	 * @param {Object} [override] options specific to the animation which will override default and animation level options
	 * @return {Mixed}
	 */
	doAnimation: function(name, override, ret_effect){
		
		var a = this._getAnimationType(name);
		if( a ){
			this._processOneTimeCallbacks(name, override);
			var options = PSWrap.Util.assembleOptions( this._options, a.options, override);
			
			/* can force the return of the effect to allow for Effect.Parallel */
			if( ret_effect){
				return new a.effect(this._element, options);
			}
			/* otherwise return the AnimatableElement to allow for chaining */
			else{
				new a.effect(this._element, options);
				return this;
			}
		}else{
			
		}
	},
	
	_processOneTimeCallbacks: function(name, options){
		options = options || {};
		
		PSWrap.Animation.HOOKS.each( function(h){
			if( Object.isFunction(options[h])){
				options[h] = options[h].wrap( function(orig, s_obj){
					this._executeCallbacks(h, s_obj, name, orig);
				}.bind(this));
			}
		});
		
	},
	
	stopCurrent: function(name){
		this._getQueue(name).entries()[0].cancel();
	},
	
	stopAll: function(name){
		this._getQueue(name).each(function(e){
			e.cancel();
		});
	},
	
	/** @ignore */
	_getQueue: function(name){
		if( name ){
			var a = this._getAnimationType(name);
			if( a ){
				if( a.options.queue && a.options.queue.scope ){
					return Effect.Queues.get(a.options.queue.scope);
				}
			}
			return null;
		}else{
			return Effect.Queues.get(this._options.queue.scope);
		}
	},
	
	/**
	 * bind an animation to occur when a certain event is observed on an element or set of elements
	 * 
	 * @param {Object} name the name of the animation to trigger
	 * @param {Object} elements the element or set of elements to bind the animation to
	 * @param {Object} event the name of the event to observe
	 * @param {Object} [override] options specific to the animation which will override default and animation level options
	 */
	bindElements: function( name, elements, event, override, ret_effect){
		if( Object.isArray(elements) ){
			elements.each( function(e){
				this._bindElement( name, e, event, override, ret_effect);
			}, this);		
		}else{
			this._bindElement( name, elements, event, override, ret_effect);
		}
	},
	
	/** @ignore */
	_bindElement: function( name, element, event, override, ret_effect, useCapture){
		var a = this._getAnimationType(name);
		if( a ){
			try{
				var e = $(element);
				e.observe( event, this._eventWrapper.bindAsEventListener(this, name, override, ret_effect), useCapture);
				a.elements.push(e);
			}catch( e ){
				
			}
		}
	},
	
	bindElementsByClass: function( options, animations){
		PSWrap.Animation.Binding.bindActionElements(this, '_bindElement', options, this._animations.keys(), animations);
	},
	
	_eventWrapper: function( evt, name, override, ret_effect){
		try{
			evt.stop();
		}catch(ex){}
		
		this[name](override, ret_effect);
	},
	
	/**
	 * Allows users to manually specify elements which perform a certain action
	 * @param {String} name
	 * @param {Object} elms
	 */
	addElements: function(name, elms){
		var a = this._getAnimationType(name);
		if( a ){
			a.elements = a.elements.concat(elms);
		}
	},
	
	/**
	 * Returns the elements bound to the animation specified by name
	 * @param {String} name
	 * @return {Object}
	 */
	getElements: function( name){
		var a = this._getAnimationType(name);
		if( a ){
			return a.elements;
		}else{
			return [];
		}
	},
	
	/**
	 * determine whether an animation type exists on an element
	 * @param {Object} name
	 * @return {Boolean}
	 */
	animationTypeExists: function (name){
		var a;
		if( (a = this._getAnimationType(name)) ){
			return a;
		}else{
			return false;
		}
	},
	
	/** @ignore */
	_getAnimationType: function( name ){
		return this._animations.get(name);
	},
	
	/**
	 * add an animation type to the object
	 * @param {String} name
	 * @param {Object} options
	 */
	registerAnimationType: function(name, options){
		if( !Object.isUndefined(this._animations[name])){
				this.removeAnimationType(name);
		}
		var obj = {
				effect: Object.isFunction(options['effect'])?options['effect']:Effect.Appear,
			   options: {},
		      elements: $A(),
			 callbacks: $H()
		}
		Object.recursiveExtend(obj.options, options.options || {});

		this._animations.set( name, obj);
		this._processCallbacks( obj.options, name);
		this.bindElementsByClass({}, [name]);
		
		/* may want to make a list of reserved words that functions can not be named */
		/* this function should still accept options and ret_effect */
		this[name] = this.doAnimation.bind(this, name);
	},
	
	addWrapperFunction: function(name, func){
		if( this._getAnimationType(name) ){
			this[name] = this[name].wrap(func);
		}
	},
	
	/* TODO: callbacks should receive the trigger element where applicable */
	_executeCallbacks: function(name, s_obj, context, func){
		if( Object.isFunction(func)	){
			func(s_obj, this);
		}
		var callbacks = $A();
		if( context ){
			c = this._getCallbacksFromContext(context);
			if (c && Object.isArray(c.get(name))){
				callbacks = callbacks.concat(c.get(name));
			}
		}
		c = this._getCallbacksFromContext();
		if( c && Object.isArray(c.get(name))){
			callbacks = callbacks.concat(c.get(name));
		}
		/*callbacks.each( function(f){
			f( s_obj, this);
		}, this);*/
		callbacks.invoke('call', null, s_obj, this);
	},
	
	addCallback: function( name, callback, context){
		var c = this._getCallbacksFromContext(context);
		if( !Object.isArray(c.get(name))){
			c.set(name, $A());
		}
		c.get(name).push(callback);
	},
	
	removeCallback: function( name, callback, context){
		var c = this._getCallbacksFromContext(context);
		/* need to make sure this actually does what it's supposed to */
		if( c && Object.isArray(c[name])){
			c[name] = c[name].without(callback);
		}
	},
	
	_processCallbacks: function(options, context){
		var c = this._getCallbacksFromContext(context);
		if (c) {
			PSWrap.Animation.HOOKS.each(function(h){
				if (Object.isFunction(options[h])){
					
					this.addCallback(h, options[h], context);
				}
				options[h] = function(s_obj){
					
					this._executeCallbacks(h, s_obj, context);
				}.bind(this);
			}, this);
		}
	},
	
	_getCallbacksFromContext: function(context){
		var a = this._getAnimationType(context);
		if( a){
			return a.callbacks;
		}else{
			return this._callbacks;
		}
	},
	
	/**
	 * removes the animation type form the element
	 * @param {String} name
	 */
	removeAnimationType: function( name){
		this._animations.remove(name);
		this[name] = null;
	},
	
	/**
	 * executes a parallel animation returning the effect or this element
	 * @param {Array} elements an array of objects of the form {element, method, options} 
	 * @param {Object} [options]
	 * @param {Integer} [ret_effect] if 1 returns the effect
	 */
	parallel: function(elements, options, ret_effect){
		var e = PSWrap.Animation.Parallel(elements, options);
		if( ret_effect ){
			return e;
		}else{
			return this;
		}
	}
});

PSWrap.Animation.DynamicLoadAnimatableElement = Class.create( PSWrap.Animation.AnimatableElement, {
	initialize: function( $super, element, options, types){
		if( PSWrap.DynamicLoader.Util.isLoadable(element)){
			this._dl = new PSWrap.DynamicLoader(element, {afterLoad: this.afterLoad.bind(this)});
			if( this._dl ){
				this._loaded = false;
				this._loading = false;
				this._load_queue = $A();
				this._load_queue_max = options['load_queue_max'] || 1;
				this._beforeLoad = options['beforeLoad'] || Prototype.emptyFunction;
				this._afterLoad = options['afterLoad'] || Prototype.emptyFunction;
			}else{
				return false;
			}
		}
		else{
			this._dl = null;
			this._loaded = true;
			this._loading = false;
		}
		$super( element, options, types);
	},
	
	isDynamicLoad: function(){
		return (this._dl?true:false);
	},
	
	getDynamicLoader: function(){
		return this._dl;
	},
	
	doAnimation: function($super, name, override, ret_effect){
		if( !this._loaded ){
			if( this._load_queue_max == 0 || this._load_queue.length < this._load_queue_max){
				this._load_queue.push({'name': name, 'override': override, 'ret_effect': ret_effect});	
			}
			if( !this._loading){
				this.loadContent();
			}
		}
		else{
			return $super(name, override, ret_effect);
		}
	},
	
	loadContent: function(){
		this._beforeLoad();
		this._dl.load();
	},
	
	afterLoad: function( dl ){
		this._afterLoad(this);
		this._loaded = true;
		this._loading = false;
		this._processLoadQueue();
	},
	
	_processLoadQueue: function(){
		this._load_queue.each( function(e){
			
			this.doAnimation( e.name, e.override, e.ret_effect);
		}, this);
		this._load_queue.clear();
	}
});

/* TODO - Parallel should check the queue of each participating element and grab the greatest finishOn,
 * this start time of the effect should be the finishOn value.  Each participating element should also
 * be locked so other animations can not start on it while it's in a parallel effect
 * 
 * need to assemble all of the beforeStart functions for each animation and put them on beforeStart
 * for parallel effect.
 */
PSWrap.Animation.Parallel = function( elements, options){
	var parallel = $A( elements || []);
	var animations = [];
	var options = PSWrap.Util.assembleOptions({
		queue: {scope: 'PSWrap_parallel', position: 'end'}
	}, options || {});
	var each_options = {
		sync: 1
	}
	parallel.each( function(e){
		if( e ){
			if( Object.isFunction(e.effect) ){
				var options = PSWrap.Util.assembleOptions( each_options, e.options || {});
				if( (ret = e.effect(options, 1))){
					animations.push(ret);
				}	
			}
		}else{
			
		}
	});
	if( animations.length ){
		var ep = new Effect.Parallel(
						animations,
		  				options
				 );
		return ep;
	}else{
		return null;
	}
};


/* need to allow conditionals more easily. possibly add a wrapper option to the types */
//DynamicLoad
PSWrap.Animation.OpenCloseElement = Class.create( PSWrap.Animation.DynamicLoadAnimatableElement,
{
	initialize: function($super, elm, options, open, close){
		/* set up the defaults so we at least have an effect */
		var types = {
			'open': {
				effect: Effect.BlindDown,
				wrapper: function(open, o, r){
					if( !this._is_open ){
						
						return open(o, r);
					}else{
						if( r ){
							return null;
						}else{
							return this;
						}
					}
				}.bind(this)
			},
			'close': {
				effect: Effect.BlindUp,
				wrapper: function(close, o, r){
					if( this._is_open ){
						
						return close(o, r);
					}else{
						if( r ){
							return null;
						}else{
							return this;
						}
					}
				}.bind(this)
			},
			'toggle': {
				effect: function(){
					if( this._is_open){
						return this.close.apply(this, arguments);
					}else{
						return this.open.apply(this, arguments);
					}
				}.bind(this)
			}
		};
		
		/* copy any passed options */
		/* TODO - this is a mess! could be taking a long time */
		//types.open = Object.recursiveExtend(types.open, open);
		//types.close = Object.recursiveExtend(types.close, close);
		Object.recursiveExtend( types.open, Object.clone(open) || {});
		Object.recursiveExtend( types.close, Object.clone(close) || {});
		
		/* options is being continuously overwritten! */
		this._options = Object.clone(options || {});
		if( Object.isUndefined(this._options.stateful) ){
			this._options.stateful = true;
		}
		
		$super( elm, this._options, types);
		/* 
		 * addWrappers to check for open before animations
		 */
		this.addWrapperFunction('open', function(open, o, r){
			if( !this._is_open ){
				
				return open(o, r);
			}else{
				if( r ){
					return null;
				}else{
					return this;
				}
			}
		}.bind(this));
		this.addWrapperFunction('close', function(close, o, r){
			if( this._is_open ){
				
				return close(o, r);
			}else{
				if( r ){
					return null;
				}else{
					return this;
				}
			}
		}.bind(this));
		
		this.addCallback('beforeStart', this._toggleOpen.bind(this));
		
		this._is_open = this.getElement().getStyle('display') == 'none'?false:true;
		if (this._options.stateful) {
			this.addCallback('beforeStart', function(s_obj, obj){
				PSWrap.State.recordState(obj);
			});
			PSWrap.State.recordState(this);
		}
	},
	
	_toggleOpen: function(){
		this._is_open = !this._is_open;
		
	},
	
	getOpenElements: function(){
		return this.getElements('open').concat(this.getElements('toggle'));
	},
	
	getCloseElements: function(){
		return this.getElements('close').concat(this.getElements('toggle'));
	},
	
	bindOpenElements: function( elements, event){
		this.bindElements('open', elements, event);
	},
	
	bindCloseElements: function( elements, event){
		this.bindElements('close', elements, event);
	},
	
	isOpen: function(){
		return this._is_open;
	},
	
	getState: function(){
		return this.getElement().readAttribute('id') + '=' + this.isOpen();
	},
	
	setState: function(s){
		
		if(s == 'true'){
			this._simpleOpen();
		}else{
			this._simpleClose();
		}
	},
	
	getId: function(){
		return this.getElement().readAttribute('id');
	},
	
	_simpleOpen: function(){
		if( !this._is_open){
			this.getOption('beforeStart', 'open')(this);
			this.getElement().show();
			this.getOption('afterFinish', 'open')(this);
		}
	},
	
	_simpleClose: function(){
		if (this._is_open) {
			this.getOption('beforeStart', 'close')(this);
			this.getElement().hide();
			this.getOption('afterFinish', 'close')(this);
		}
	}
});

PSWrap.Animation.OpenCloseIDBound = Class.create( PSWrap.Animation.OpenCloseElement, {
	initialize: function($super, elm, options, open, close){
		$super(elm, options, open, close);
		/*this._open_class = this.getElement().readAttribute('id') + '_action_open';
		this._close_class = this.getElement().readAttribute('id') + '_action_close';
		this._toggle_class = this.getElement().readAttribute('id') + '_action_toggle';
		var open_elements = $$('.' + this._open_class);
		var close_elements = $$('.' + this._close_class);
		var toggle_elements = $$('.' + this._toggle_class);
		this.bindElements('open', open_elements, 'click');
		this.bindElements('close', close_elements, 'click');
		this.bindElements('toggle', toggle_elements, 'click');
		open_elements.invoke('removeClassName', this._open_class);
		close_elements.invoke('removeClassName', this._close_class);
		toggle_elements.invoke('removeClassName', this._toggle_class);*/
	}
});

/* IDEA: collection should not have to grab a bunch of elements, a collection should be PASSED a bunch of elements with the possibility to add and remove */
/* want to hide all elements that are not current here? */
PSWrap.Animation.OpenCloseCollection = Class.create({
	initialize: function(container, collection_options, options, open_options, close_options){
		/* collection options can include whether this should be parallel, each queue, this queue */
		this._collection_options = Object.extend({
				item_selector: '.item',
				only_child_items: true
			}, collection_options || {});
		this._options = PSWrap.Util.assembleOptions( options, {stateful: false});
		this._elements = [];
		this._current_item = null;
		this._scope = 'parallel';
		this._animation_type = this._collection_options['animation_type'] || PSWrap.Animation.PARALLEL;
		this._position_items = true;
		if( !Object.isUndefined(this._collection_options['position_items'])){
			this._position_items = this._collection_options['position_items'];
		}
		if( Object.isUndefined(this._collection_options['stateful'])){
			this._collection_options['stateful'] = true;
		}
		
		open_options = open_options || {};
		close_options = close_options || {};
		
		this._container = $(container);
		var cid = this._container.identify();
		if( this._container && cid){
			this._container.makePositioned();
			this._scope = cid;
			this._scope_options = {queue: {scope: this._scope}};
			var items = null;
			if (this._collection_options.only_child_items) {
				items = this._container.selectChildren(this._collection_options.item_selector);
			}
			else {
				items = this._container.select(this._collection_options.item_selector);
			}
			var l = items.length;
			items.each( function(e){
				var oce = new PSWrap.Animation.OpenCloseElement(e, this._options, open_options, close_options);
				if( oce ){
					oce.addWrapperFunction('open', function( proceed, o, r){
						this._doOpen(proceed, oce);
					}.bind(this));
					
					oce.addCallback('beforeStart', function(){
						this._setCurrent(oce);
					}.bind(this), 'open');
					oce.addCallback('beforeSetup', function(){
						if( this._position_items){
							this._changeContainerHeight(oce);
							oce.getElement().setStyle({position: 'absolute', top: '0px', left: '0px'});
						}
					}.bind(this), 'open');
					
					oce.addCallback('afterFinish', function(){
						if( this._position_items ){
							oce.getElement().setStyle({position: '', top: '', left: ''});
							this._changeContainerHeight();
						}
					}.bind(this), 'open');
					
					if( this._collection_options['stateful']){
						oce.addCallback('beforeStart', function(){
							PSWrap.State.recordState(this);
						}.bind(this), 'open');
					}
					
					var id = oce.getElement().readAttribute('id');
					if( oce.isOpen() ){
						if (!this.getCurrent()) {
							this._setCurrent(oce);
						}else{
							/* may want to do hide() here instead */
							oce._simpleClose(this._scope_options);
						}
					}
					this._elements.push(oce);
				}
			}, this);
			
			var c = this._container.readAttribute('id') + '_action_close';
			this._close_elements = $$('.' + c);
			this._close_elements.invoke('observe', 'click', this.close.bindAsEventListener(this));
			this._close_elements.invoke('removeClassName', c);
			if( this._collection_options['stateful'] ){
				PSWrap.State.recordState(this);
			}
		}else{
			
		}
	},
	
	_doOpen: function(proceed, elm){
		var c = null;
		if( (c = this.getCurrent()) && elm != c){
			if (this._animation_type == PSWrap.Animation.SEQUENTIAL) {
				/* if it's sequential, just overwrite the queue options to put them in the queue
				 * for the collection.
				 */
				var options = {queue: {scope: this._scope}};
				c.close(options);
				proceed(options);
			}
			else {
				/* if this is a parallel animation, use the global options for the animation, be sure to overwrite the scope
	 			 */
				var options = PSWrap.Util.assembleOptions(this._options, {
					queue: {
						scope: this._scope
					}
				});
				/* since these options have been used on each OpenCloseElement, we don't want more event handlers declared
	 			 * for the parallel. remove them here.
	 			 */
				//PSWrap.Util.removeOptions(this._options, ['beforeStart', 'afterFinish', 'beforeUpdate', 'afterUpdate']);
				PSWrap.Util.removeOptions(options, ['beforeStart', 'afterFinish', 'beforeUpdate', 'afterUpdate', 'transition']);
				PSWrap.Animation.Parallel([{
					effect: proceed
				}, {
					effect: c.close
				}], options);
			}
		}else{
			proceed({
				queue: {
					scope: this._scope
				}
			});
		}
	},
	
	_simpleOpen: function(elm){
		var c = null;
		if( (c = this.getCurrent()) && elm != c ){
			c._simpleClose();
		}
		if( elm ){
			this._setCurrent(elm);
			elm._simpleOpen();
		}
	},
	
	_setCurrent: function( elm ){
		if( elm ){
			
			/* changed here */
			this._current_item = elm;
		}
	},
	
	getCurrent: function(){
		return this._current_item;
	},
	
	_changeContainerHeight: function(elm){
		if( elm ){
			current_height = this._container.getHeight();
			new_height = elm.getElement().getHeight();
			
			this._container.setStyle({height: new_height + 'px'});
		}else{
			/* CHANGED HERE */
			this._container.setStyle({height: 'auto'});
		}
	},
	
	close: function(e){
		try{
			e.stop();
		}catch(ex){}
		if( this.getCurrent() ){
			this.getCurrent().close(this._scope_options);
			this._setCurrent(null);
		}
	},
	
	getState: function(){
		if( this.getCurrent()){
			return this._container.readAttribute('id') + '=' + this._elements.indexOf(this.getCurrent());
		}else{
			return '';
		}
	},
	
	setState: function(i){
		if( i > 0 && i < this._elements.length){
			/* could potentially add options at the end of _doOpen to pass one time callbacks */
			this._simpleOpen(this._elements[i]);
		}
	},
	
	getId: function(){
		return this._container.readAttribute('id');
	},
	
	identify: function(){
		return this.getId();
	},
	
	observe: function(){
		return this._container.observe.apply(this._container, arguments);
	},
	
	fire: function(){
		return this._container.fire.apply(this._container, arguments);
	}
});

PSWrap.Animation.SequentialOpenCloseCollection = Class.create( PSWrap.Animation.OpenCloseCollection, {
	initialize: function( $super, container, collection_options, options, open_options, close_options){
		$super( container, collection_options, options, open_options, close_options);
		this._current_index = this._collection_options['startindex'] || 0;
		this._wrap = this._collection_options['wrap'] || 1;
		
		this._elements.invoke('addCallback', 'beforeStart', this._updateDisplayCurrentElements.bind(this), 'open');
		
		/*var c = this._container.readAttribute('id') + '_action_next';
		this._next_elements = $$( '.' + c);
		this._next_elements.invoke('removeClassName', c);
		
		c = this._container.readAttribute('id') + '_action_previous';
		this._prev_elements = $$( '.' + c);
		this._prev_elements.invoke('removeClassName', c);*/
		
		var elements = PSWrap.Animation.Classnames.getElements(this, 'display', ['current', 'total'], true);
		this._display_current_elements = elements['current'];
		this._display_total_elements = elements['total'];
		
		
		
		if( this._current_index != this._elements.indexOf(this.getCurrent())){
			//this._doOpen(null, this._elements[this._current_index]);
			this._elements[this._current_index].open();
		}
		
		elements = PSWrap.Animation.Binding.bindActionElements(this, '_bindElement', {}, ['previous', 'next'], ['previous', 'next']);
		this._next_elements = elements['next'] || [];
		this._previous_elements = elements['previous'] || [];
		
		this._updateDisplayTotalElements();
		this._updateDisplayCurrentElements();
	},
	
	_bindElement: function(method, element, event, options, ret_effect, useCapture){
		//console.log('_bindElement: ' + element + ', ' + event)
		try{
			var e = $(element);
			e.observe( event, this[method].bindAsEventListener(this, options, ret_effect), useCapture);
		}catch( e ){
			
		}
	},
	
	previous: function(e, options, ret_effect){
		try{
			e.stop();
		}catch(ex){}
		var new_index = this._current_index - 1;
		new_index = new_index<0? this._wrap?this._elements.length-1:0 : new_index;
		
		if( new_index != this._current_index ){
			this._current_index = new_index;
			//this._doOpen(null, this._elements[this._current_index]);
			this._elements[this._current_index].open(options, ret_effect);	
		}
	},
	
	next: function(e, options, ret_effect){
		if (e) {
			e.stop();
		}
		var new_index = this._current_index + 1;
		new_index = new_index>this._elements.length - 1? !this._wrap?this._elements.length-1:0 : new_index;
		
		if( new_index != this._current_index ){
			this._current_index = new_index;
			//this._doOpen(null, this._elements[this._current_index]);
			this._elements[this._current_index].open(options, ret_effect);
		}
	},
	
	_updateDisplayCurrentElements: function(s_obj, oc_obj){
		var i = this._current_index + 1;
		if( oc_obj ){
			i = this._elements.indexOf(oc_obj) + 1;
		}
		this._display_current_elements.invoke('update', i);
	},
	
	_updateDisplayTotalElements: function(){
		this._display_total_elements.invoke('update', this._elements.length);
	}
});

PSWrap.Event = {
	'start'		: 'pswrap:start',
	'stop'		: 'pswrap:stop'
}

PSWrap.Animation.AutoOpenCloseCollection = Class.create( PSWrap.Animation.SequentialOpenCloseCollection, {
	initialize: function($super, container, collection_options, options, open_options, close_options){
		$super(container, collection_options, options, open_options, close_options);
		this._time = this._collection_options['time'] || 3;
		this._stopafter = this._collection_options['stopafter'] || 0;
		this._interval = null;
		this._iterations = 0;
		this._direction = this._collection_options['direction'] || PSWrap.Animation.Directions.FORWARD;
		this._running = false;
		if( this._current_item ){
			this._current_index = this._elements.indexOf(this._current_item);
		}else{
			this._current_index = 0;
		}
		if( this._collection_options['autostart']){
			this.start();
		}
		/* need to look for start and stop links */
		var elements = PSWrap.Animation.Binding.bindActionElements(this, '_bindElement', {}, ['stop', 'start'], ['stop', 'start']);
		this._start_elements = elements['start'] || [];
		this._stop_elements = elements['stop'] || [];
	},
	
	previous: function($super){
		var stop = arguments.length==2?arguments[1]:true;
		if (stop) {
			this.stop();
		}
		$super();
	},
	
	next: function($super){
		var stop = arguments.length==2?arguments[1]:true;
		if (stop) {
			this.stop();
		}
		$super();
	},
	
	start: function(){
		this.stop();
		this._iterations = 0;
		/*
		 * Firefox has a bug in setInterval which causes erratic behavior
		 * when multiple intervals are set. for now use setTimeout
		 * 
		 * this._interval = new PeriodicalExecuter(this.swap.bind(this), this._time);*/
		this._running = true;
		this._interval = setTimeout( this.swap.bind(this), this._time * 1000);
		this.fire(PSWrap.Event.start, {
			elements:{
				'start': this._start_elements,
				'stop': this._stop_elements
			}
		});
	},
	
	stop: function(){
		try{
			/* this._interval.stop();
			 * delete this._interval;*/
			this._running = false;
			this.fire(PSWrap.Event.stop, {
				elements:{
					'start': this._start_elements,
					'stop': this._stop_elements
				}
			});
		}catch(ex){}
	},
	
	swap: function(){
		if (this._running) {
			if (this._direction < 0) {
				this.previous(false);
			}
			else {
				this.next(false);
			}
			this._iterations++;
			if (this._iterations == this._stopafter) {
				this.stop();
			}
			else {
				this._interval = setTimeout(this.swap.bind(this), this._time * 1000);
			}
		}
	}
});

PSWrap.Animation.MovableCollection = Class.create({
	initialize: function(container, options){
		this._container = $(container);
		this._options = Object.extend({
				'loop': true
			},options || {});
		
		if( Object.isUndefined(this._options.direction) ){
			this._options.direction = PSWrap.Animation.Directions.VERTICAL;
		}
		if( this._container && this._container.readAttribute('id')){
			this._container.makePositioned().makeClipping();
			this._original_offset = this._container.positionedOffset();
			this._content = this._container.selectChildren('#' + this._container.readAttribute('id') + '_content')[0];
			this._goto_elements = $H();
			if( !this._content){
				var content = new Element('div', {id: this._container.readAttribute('id') + '_content'});
				this._container.selectChildren('.item').each( function(i){
					content.insert(i.remove());
				});
				this._content = content;
				this._container.insert(this._content);
			}
			if( this._content ){
				this._content.makePositioned();
				this._children = this._content.selectChildren('.item');
				var temp = this._content.getStyle('overflow');
				this._content.setStyle({'overflow': 'auto'});
				this._offsets = this._children.invoke('positionedOffset');
				this._content.setStyle({'overflow': temp});
				
				if (this._options.direction == PSWrap.Animation.Directions.HORIZONTAL) {
					this._children.invoke('setStyle', {'float': 'left'});
					this._initWidth(1);
				}
				
				this._current_index = 0;
				
				this._element = new PSWrap.Animation.AnimatableElement( this._content, {}, {move: {effect: Effect.Move, options: {mode: 'absolute', transition: Effect.Transitions.sinoidal}}});
				
				var c = this._container.readAttribute('id') + '_action_previous';
				this._prev_elements = $$('.' + c);
				this._prev_elements.invoke('observe', 'click', this.prev.bindAsEventListener(this));
				this._prev_elements.invoke('removeClassName', c);
				
				c = this._container.readAttribute('id') + '_action_next';
				this._next_elements = $$('.' + c);
				this._next_elements.invoke('observe', 'click', this.next.bindAsEventListener(this));
				this._next_elements.invoke('removeClassName', c);
				
				c = this._container.readAttribute('id') + '_display_current';
				this._display_current_elements = $$( '.' + c);
				this._display_current_elements.invoke('removeClassName', c);
				
				c = this._container.readAttribute('id') + '_display_total';
				this._display_total_elements = $$( '.' + c);
				this._display_total_elements.invoke('removeClassName', c);
				this._display_total_elements.invoke('update', this._children.length);
				this._display_current_elements.invoke('update', '1');
				
				c = this._container.readAttribute('id') + '_goto';
				var elements = $$('[class^=' + c + ']');
				var reg = new RegExp('^' + c + '_(.+)$');
				elements.each( function(elm){
					$w(elm.className).each( function(c){
						var result = reg.exec(c);
						if( result ){
							if( /^[0-9]+$/.test(result[1])){
								var index = parseInt(result[1]);
								this._addGoToElement(index, elm);
								elm.observe('click', this.goToItem.bindAsEventListener(this, index));
							}else if(Object.isString(result[1])) {
							 	var item = this._content.down('#' + result[1]);
							 	if (item) {
							 		var index = this._children.indexOf(item);
							 		this._addGoToElement(index, elm);
									elm.observe('click', this.goToItem.bindAsEventListener(this, index));
							 	}
							}
							elm.removeClassName(c);
						}
					}.bind(this));
				}.bind(this));
			}else{
				return null;
			}
		}else{
			return null;
		}
	},
	
	_addGoToElement: function(index, e){
		var curr = this._goto_elements.get(index);
		if( curr && Object.isArray(curr) ){
			curr.push(e);
		}else{
			curr = new Array(e);
		}
		this._goto_elements.set(index, curr);
	},
	
	_initWidth: function( force ){
		if (this._options.direction == PSWrap.Animation.Directions.HORIZONTAL && (force || this._content.getWidth() == 0)) {
			var temp = new Element('div', {
				style: 'float: left; width: 20px; border: 1px solid white;'
			});
			this._content.setStyle({
				width: '100000px'
			});
			this._children.last().insert({
				after: temp
			});
			var width = temp.positionedOffset()[0];
			temp.remove();
			this._content.setStyle({
				width: width + 'px'
			});
		}
	},
	
	goToItem: function(){
		var item;
		if( arguments.length == 2 ){
			try{
				arguments[0].stop();
			}catch(ex){}
			item = arguments[1];
		}else{
			item = arguments[0];
		}
		if( /^[0-9]+$/.test(item) ){
			this._moveToIndex(parseInt(item));
		}else if( Object.isString(item) ){
			item = this._content.down('#' + item);
			if( item ){
				var index = this._children.indexOf(item);
				this._moveToIndex(parseInt(index));
			}
		}
	},
	
	_switchActive: function( old_index, new_index){
		var elms = this._goto_elements.get(old_index);
		if( elms ){
			//console.log(elms);
			elms.invoke('removeClassName', 'active');
		}
		elms = this._goto_elements.get(new_index);
		if(elms){
			//console.log(elms);
			elms.invoke('addClassName', 'active');
		}
	},
	
	_moveToIndex: function(new_index){
		if( new_index != this._current_index){
			if( this._options.direction == PSWrap.Animation.Directions.HORIZONTAL){
				
				var new_x = this._offsets[new_index][0];
				if( new_x == 0 && new_index != 0){
					this._offsets[new_index] = this._children[new_index].positionedOffset();
					new_x = this._offsets[new_index][0];
				}
				
				//console.log(('new x: ' + new_x + ', ' + this._container.getWidth()));
				//if( this._content.getWidth() - new_x >= this._container.getWidth()){
					var options = Object.extend( this._options, {x: -new_x});
					this._element.move(options);
					this._switchActive(this._current_index, new_index);
					this._current_index = new_index;
				//}
			}else{
				var new_y = this._offsets[new_index][1];
				if( new_y == 0 && new_index!= 0){
					this._offsets[new_index] = this._children[new_index].positionedOffset();
					new_y = this._offsets[new_index][1];
				}
				//if( this._content.getHeight() - new_y >= this._container.getHeight() ){
					var options = Object.extend( this._options, {y: -new_y});
					this._element.move(options);
					this._switchActive(this._current_index, new_index);
					this._current_index = new_index;
				//}
			}
			this._display_current_elements.invoke('update', this._current_index + 1);
		}
	},
	
	next: function(e){
		try{
			e.stop();
		}catch(ex){}
		this._initWidth();
		var new_index = this._current_index + 1;
		if( new_index >= this._offsets.length){
			if( this._options.loop ){
				new_index = 0;
			}else{
				new_index = this._offsets.length - 1;
			}
		}
		this._moveToIndex(new_index);
	},
	
	prev: function(e){
		try{
			e.stop();
		}catch(ex){}
		this._initWidth();
		var new_index = this._current_index - 1;
		if( new_index < 0){
			if( this._options.loop ){
				new_index = this._offsets.length - 1;
			}else{
				new_index = 0;
			}
		}
		this._moveToIndex(new_index);
	}
});

/* based on code @ http://www.webtoolkit.info/ajax-file-upload.html */
PSWrap.AjaxUpload = Class.create({
	initialize: function(elm, opts){
		this._input_element = $(elm);
		this._element = this._input_element.up('form');
		if( this._element && this._input_element.readAttribute('id')){
			opts = opts || {};
			this.onStart = Object.isFunction(opts['onStart'])?opts['onStart']:Prototype.emptyFunction;
			this.onComplete = Object.isFunction(opts['onComplete'])?opts['onComplete']:Prototype.emptyFunction;
			this._frame_action = !Object.isUndefined(opts['action'])?opts['action']:'';
			this._frame_id = this._input_element.readAttribute('id') + '_iframe';
			if ($(this._frame_id)) {
				var i = 1;
				var id = this._frame_id + i;
				while( $(id) ){
					i++;
					id = this._frame_id + i;
				}
				this._frame_id = id;
			}
			this._frame = new Element('iframe', {
					id: this._frame_id,
					name: this._frame_id,
					src: 'about:blank',
					style: 'display: none'
				});
			$$('body')[0].appendChild(this._frame);
			this._frame.observe('load', this.load.bindAsEventListener(this));
		}else{
			return false;
		}
	},
	
	submit: function(e){
		if (this._input_element.value != '') {
			var old_action, old_target;
			// grab current action
			old_action = this._element.readAttribute('action');
			if (this._frame_action) {
				// if we have an alternate action, set it
				this._element.setAttribute('action', this._frame_action);
			}
			// grab old target
			old_target = this._element.readAttribute('target');
			// set hidden iframe target
			this._element.setAttribute('target', this._frame_id);
			// execute before start
			this.onStart();
			// submit the form
			this._element.submit();
			this._input_element.setAttribute('disabled', 'disabled');
			// reset originals
			this._element.setAttribute('action', old_action);
			this._element.setAttribute('target', old_target);
		}
	},
	
	load: function(e){
		this._input_element.value = '';
		this._input_element.writeAttribute('disabled', null);
		if (this._frame.contentDocument) {
            var d = this._frame.contentDocument;
        } else if (this._frame.contentWindow) {
            var d = this._frame.contentWindow.document;
        } else {
            var d = window.frames[this._frame.readAttribute('id')].document;
        }
		if (d.location.href == "about:blank") {
            return;
        }
        this.onComplete(d.body.innerHTML, this._input_element);
	}
});

PSWrap.FormControls = {};
PSWrap.FormControls.SelectBase = Class.create({
	initialize: function(elm, options){
		elm = $(elm);
		if( elm && elm.tagName == 'SELECT'){
			this._elm = elm;
			this._data = $H();
			this._select_options = $H();
			this._currently_selected = [];
			
			this._num_selected = 0;
			this._options = Object.extend({
				max_selected: 0,
				min_selected: 0
			}, options || {});
			
			this._multiple = this._elm.readAttribute('multiple');
			if( !this._multiple ){
				this.options.max_selected = 1;
				this.options.min_selected = 1;
			}
			if( this._elm.options.length ){
				$A(this._elm.options).each( function(o){
					this._select_options.set(o.value, {option: $(o)});
				}, this);
			}
		}else{
			throw "PSWrap.FormControls.SelectBase: element is not a select object";
		}
	},
	addOption: function( text, value, selected){
		/*
		 * this doesn't work in IE:
		 * var opt = new Option( text, value, false, selected);
		 */
		var att = {
			'value': value
		}
		if( selected ) att.selected = 'selected';
		var opt = new Element('option', att).update(text);
		if(opt){
			this._elm.insert(opt);
			opt = this._select_options.set( value, {option: opt});
			if( selected ){
				this._selectOption(opt);
			}
		}
	},
	removeOption: function( value ){
		var opt = this.getOption(value);
		if( opt ){
			if( opt.option.selected ){
				this._deselectOption(opt);
			}
			opt.option.remove();
			this._select_options.unset( value);
		}
	},
	getOption: function( value ){
		return this._select_options.get(value);
	},
	selectOption: function( value ){
		var selected = true;
		if( arguments.length == 2 ) selected = arguments[1];
		var opt = this._select_options.get(value);
		
		if( opt.option.selected != selected ){
			if( selected ){
				if( this._multiple ){
					if (!this._max_selected || this._num_selected < this._max_selected) {
						this._selectOption(opt);
					}
				}else{
					var cs = this._currently_selected.pop();
					if( cs ){
						this._deselectOption( this._select_options.get(cs) );
					}
					this._selectOption( opt );
				}
			}else{
				this._deselectOption(opt);
			}
		}
	},
	_selectOption: function(opt){
		opt.option.selected = true;
		this._currently_selected.push( opt.option.value );
	},
	_deselectOption: function( opt ){
		opt.option.selected = false;
		this._currently_selected = this._currently_selected.without(opt.option.value);
	},
	identify: function(){
		return this._elm.identify();
	}
});

PSWrap.FormControls.SingleContainerMultiselects = {
	_items: new Hash(),
	add: function(elm){
		elm = $(elm);
		if( elm ){
			PSWrap.FormControls.SingleContainerMultiselects._items.set(elm.identify(), elm);
		}
	},
	remove: function( id ){
		if( Object.isElement(id) ){
			id = id.identify();
		}
		var r = PSWrap.FormControls.SingleContainerMultiselects._items.get(id);
		PSWrap.FormControls.SingleContainerMultiselects._items.set(id, null);
		return r;
	},
	get: function(id){
		return PSWrap.FormControls.SingleContainerMultiselects._items.get(id);
	},
	items: function(){
		return PSWrap.FormControls.SingleContainerMultiselects._items.values();
	}
};
PSWrap.FormControls.SingleContainerMultiselect = Class.create( PSWrap.FormControls.SelectBase, {
	initialize: function( $super, elm, container_template, item_template, options){
		options = Object.extend({
			selected_class: 'selected',
			prevent_default: false,
			select_element: null
		}, options || {});
		elm.setAttribute('multiple', 'multiple');
		$super( elm, options);
		if( this._options.select_event && this._options.select_event == this._options.deselect_event ){
			this._options.toggle_event = this._options.select_event;
			this._options.select_event = this._options.deselect_event = null;
		}
		this._elements = $H();
		if( container_template instanceof Template && item_template instanceof Template){
			this._elm.hide();
			this._id = this._elm.identify() + '_single_container_multiselect';
			
			var container = container_template.evaluate({'id': this._id});
			this._elm.insert({'after': container});
			(function(){
				this._container = $(this._id);
				if( !container ) throw 'PSWrap.FormControls.SingleContainerMultiselect: container not found, please make sure the specified template is properly setting the id -> id="#{id}"';
				this._item_template = item_template;
				
				if (this._elm.options.length) {
					$A(this._elm.options).each(function(o){
						this.addOptionElement(o.text, o.value, o.selected);
					}, this);
				}
			}).bind(this).defer();
			PSWrap.FormControls.SingleContainerMultiselects.add(this);
		}else{
			throw 'PSWrap.FormControls.SingleContainerMultiselect: container_template and item_template must be of type Template';
		}
	},
	addOption: function( $super, text, value, selected){
		$super(text, value, selected);
		this.addOptionElement(text, value, selected);
	},
	addOptionElement: function(text, value, selected){
		var id = this._id + '_' + value;
		var data = {
			'text': text,
			'value': value,
			'id': id,
			'selected': selected
		};
		var option =  this._item_template.evaluate(data);
		this._container.insert(option);
		this._addOptionElement.bind(this).defer(id, value, selected);
	},
	_addOptionElement: function(id, value, selected){
		//var option = this._container.childElements().last();
		var option = $(id);
		var elm = null;
		if( this._options.select_element ){
			elm = option.down(this._options.select_element);	
		}
		elm = elm || option;
		if( this._options.toggle_event ){
			try{
				elm.observe(this._options.toggle_event, this.toggleOptionEventWrapper.bindAsEventListener(this, option, value));
			}catch(ex){}
		}else{
			if( this._options.select_event ){
				try{
					elm.observe(this._options.select_event, this.selectOptionEventWrapper.bindAsEventListener(this, option, value, 1));
				}catch(ex){}
			}
			if( this._options.deselect_event ){
				try{
					elm.observe(this._options.deselect_event, this.selectOptionEventWrapper.bindAsEventListener(this, option, value, 0));
				}catch(ex){}
			}
		}
		this._elements.set( value, option);
		this._elm.fire('pswrap:optionadded', {element: option, option: this._select_options.get(value), select: this});
		if( selected ){
			this.selectOption(option, value, selected);
		}
	},
	removeOption: function( $super, value){
		$super(value);
		var del = this._elements.get(value);
		if( del ) del.remove();
		this._elements.unset(value);
		this._elm.fire('pswrap:optionremoved', {element: del, option: null, select: this});
		this._elm.hide();
	},
	toggleOptionEventWrapper: function( e, option, value){
		if( this._options.prevent_default ){
			try{
				e.stop();
			}catch(ex){}
		}
		var selected = !this._select_options.get(value).option.selected;
		this.selectOption( option, value, selected);
	},
	selectOptionEventWrapper: function(e, option, value, selected){
		if ( this._options.prevent_default ) {
			try {
				e.stop();
			} 
			catch (ex) {}
		}
		this.selectOption(option, value, selected);
	},
	selectOption: function($super, option, value, selected){
		$super(value, selected);
		if( selected ){
			option.addClassName(this._options.selected_class);
			this._elm.fire('pswrap:optionselected', {'element': option, 'option': this._select_options.get(value), 'select': this});
		}else{
			option.removeClassName(this._options.selected_class);
			this._elm.fire('pswrap:optiondeselected', {'element': option, 'option': this._select_options.get(value), 'select': this});
		}
	}
});
PSWrap.FormControls.DragDropMultiselects = {
	_items: new Hash(),
	add: function(elm){
		elm = $(elm);
		if( elm ){
			PSWrap.FormControls.DragDropMultiselects._items.set(elm.identify(), elm);
		}
	},
	remove: function( id ){
		if( Object.isElement(id) ){
			id = id.identify();
		}
		var r = PSWrap.FormControls.DragDropMultiselects._items.get(id);
		PSWrap.FormControls.DragDropMultiselects._items.set(id, null);
		return r;
	},
	get: function(id){
		return PSWrap.FormControls.DragDropMultiselects._items.get(id);
	}
};
PSWrap.FormControls.DragDropMultiselect = Class.create({
	initialize: function(elm, template, options){
		this._selection_element = $(elm);
		if( this._selection_element	&& this._selection_element.tagName == 'SELECT'){
			if( !(template instanceof Template) ){
				throw "PSWrap.FormControls.DrapDropMultiselect initialize: template is not of type Template.";
				return null;
			}else{
				this._template = template;
			}
			this._options = $A();
			this._selected = $A();
			this._options_by_value = $H();
			this._currently_selected = 0;
			this._draggables = $H();
			
			this._control_options = Object.extend({
				remove_event: 'dblclick',
				add_event: 'dblclick',
				options_title: null,
				selected_title: null,
				onInit: Prototype.emptyFunction,
				onOptionChange: Prototype.EmptyFunction,
				onOptionAdd: Prototype.emptyFunction,
				onOptionRemove: Prototype.emptyFunction,
				onSelectedChange: Prototype.emptyFunction,
				onSelectedAdd: Prototype.emptyFunction,
				onSelectedRemove: Prototype.emptyFunction,
				max_selected: 0
			}, options || {});
		
			this._selection_element.hide();
			this._id = this._selection_element.identify();
			
			var container = new Element('div', {'class': 'drag_drop_multiselect'});
			
			var otitle = this._control_options.options_title;
			if( otitle ){
				otitle = new Element('div', {'class': 'options_title'}).update(otitle);
			}
			this._options_container = new Element('div', {
				'id': this._id + '_options', 
				'class': 'options'
			});
			
			var stitle = this._control_options.selected_title;
			if( stitle ){
				stitle = new Element('div', {'class': 'selected_title'}).update(stitle);
			}
			this._selected_container = new Element('div', {
				'id': this._id + '_selected',
				'class': 'selected'
			});
			
			if( stitle ){
				container.insert(stitle);
			}
			container.insert(this._selected_container);
			if( otitle ){
				container.insert(otitle);
			}
			container.insert(this._options_container);
			this._selection_element.insert({after: container});
			
			$A(this._selection_element.options).each( function(o){
				this._addItem( o.value, o.text, false);
				if( o.selected ){
					this._addItem( o.value, o.text, true);
				}
				this._options_by_value.set( o.value, o);
			}.bind(this));
			Droppables.add(this._selected_container, {
				containment: this._id + '_options', 
				onDrop: this.select.bind(this)
			});
			PSWrap.FormControls.DragDropMultiselects.add(this);
			this._fireCallback('onInit', this);
		}else{
			throw "PSWrap.FormControls.DragDropMultiselect.initialize: Invalid select element";
		}
	},
	identify: function(){
		return this._id;
	},
	getID: function(){
		return this._id;
	},
	getSelectedContainer: function(){
		return this._selected_container;
	},
	getSelectedItems: function(){
		return this._selected;
	},
	getOptionsContainer: function(){
		return this._options_container;
	},
	getOptionsItems: function(){
		return this._options;
	},
	select: function(elm){
		var val = elm.identify().split('_').last();
		if( val ){
			if(this._control_options['max_selected'] == 0 || this._currently_selected < this._control_options['max_selected']) {
				var o = this._options_by_value.get(val);
				try {
					if (!o.selected) {
						o.selected = true;
						var img = this._addItem(val, elm.src, true);
						this._currently_selected++;
						this._fireCallback('onSelectedAdd', img, val, this._selected_container);
						this._fireCallback('onSelectedChange', img, val, this._selected_container);
					}
				} 
				catch (e) {
				}
			}
		}
	},
	deselect: function(elm){
		var val = elm.identify().split('_').last();
		if( val ){
			var o = this._options_by_value.get(val);
			try{
				if( o.selected ){
					o.selected = false;
					elm = this._removeItem( elm, true);
					this._currently_selected--;
					this._fireCallback('onSelectedRemove', elm, val, this._selected_container);
					this._fireCallback('onSelectedChange', elm, val, this._selected_container);
				}
			}catch(e){}
		}
	},
	addElement: function( val, uri, selected){
		if (!this._options_by_value.get(val)) {
			if( Object.isUndefined(selected)){
				selected = false;
			}
			var o = this._addOption(val, uri, selected);
			var elm = this._addItem(val, uri, selected);
			this._options_by_value.set(o.value, o);
			//console.log('firing callbacks!');
			this._fireCallback('onOptionAdd', elm, val, this._options_container);
			this._fireCallback('onOptionChange', elm, val, this._options_container);
			//console.log('fired callbacks');
		}
	},
	removeElement: function(elm){
		elm = $(elm);
		var id = elm.identify().split('_').last();
		this.removeElementById(id, elm);
	},
	removeElementById: function(id, elm){
		var selm = this._getSelectedItemFromValue(id);
		if( selm ){
			this.deselect(selm);
		}
		if( !elm ){
			elm = this._getOptionItemFromValue(id);
		}
		this._removeOption( elm, id);
	},
	_getSelectedItemFromValue: function(v){
		return this._getItemFromValue( v, this._selected);
	},
	_getOptionItemFromValue: function(v){
		return this._getItemFromValue( v, this._options);
	},
	_getItemFromValue: function( v, arr){
		var l = arr.length;
		for( var i=0; i<l; i++){
			if( arr[i].identify().split('_').last() == v){
				return arr[i];
			}
		}
		return null;
	},
	_removeOption: function( elm, val){
		if( !val ){
			val = elm.identify().split('_').last();
		}
		try{
			var o = this._options_by_value.get(val);
			if( o ){
				//console.log('about to remove option');
				o.remove();
				//console.log('removed option!');
				elm = this._removeItem(elm, false);
				this._fireCallback('onOptionRemove', elm, val, this._options_container);
				this._fireCallback('onOptionChange', elm, val, this._options_container);
			}
		}catch(ex){
			//console.log(ex);
		};
	},
	_addOption: function( val, uri, selected){
		if( val && uri){
			var opt = new Element('option', {value: val}).update(uri);
			opt.selected = selected;
			this._selection_element.insert({bottom: opt});
			return opt;
		}else{
			return null;
		}
	},
	_removeItem: function(elm, selected){
		var l, i, arr, evt, obs;
		if( selected ){
			this._selected = this._selected.without(elm);
			evt = this._control_options.remove_event;
			obs = '_remove_observer';
		}else{
			this._options = this._options.without(elm);
			evt = this._control_options.add_event;
			obs = '_add_observer';
			if( elm._draggable ){
				elm._draggable.destroy();
			}
		}
		try{
			elm.stopObserving( evt, elm[obs]);
			return elm.remove();
		}catch(ex){
			return null;
		};
	},
	_addItem: function( val, uri, selected){
		if (uri) {
			var bucket = this._getBucket(selected);
			var id = bucket.identify() + '_' + val;
			var data = {
				value: val,
				text: uri,
				id: id
			};
			var elm = this._template.evaluate(data);
			bucket.insert({
				bottom: elm
			});
			elm = $(id);
			try{
				if( !selected ){
					elm._draggable = new Draggable( elm, {
						revert: true,
						ghosting: false,
						onStart: Prototype.emptyFunction,
						onEnd: Prototype.emptyFunction,
						scroll: window
					});
					elm._add_observer = this._selectEventWrapper.bind(this);
					elm.observe( this._control_options.add_event, elm._add_observer);
					this._options.push(elm);
				}else{
					var o = this._deselectEventWrapper.bind(this);
					elm._remove_observer = o;
					elm.observe( this._control_options.remove_event, o);
					this._selected.push(elm);
				}
				return elm;
			}catch(ex){
				//console.log(ex);
			}
		}else{
			return null;
		}
	},
	_getBucket: function(selected){
		return selected?this._selected_container:this._options_container;
	},
	_deselectEventWrapper: function(evt){
		this.deselect(evt.element());
	},
	_selectEventWrapper: function(evt){
		this.select(evt.element());
	},
	_fireCallback: function(){
		var arguments = $A(arguments);
		if( arguments.length ){
			var f = arguments.shift();
			if( Object.isFunction( this._control_options[f])){
				this._control_options[f].apply( null, arguments);
			}else if( Object.isArray( this._control_options[f])){
				var arr = this._control_options[f];
				var l = arr.length;
				for( var i=0; i<l; i++){
					if( Object.isFunction(arr[i])){
						arr[i].apply( null, arguments);
					}
				}
			}
		}
	}
});
PSWrap.FormControls.ImageSelectionBuckets = Class.create({
	initialize: function(elm, options){
		this._selection_element = $(elm);
		this._options_images = $A();
		this._selected_images = $A();
		this._options_by_value = $H();
		this._currently_selected = 0;
		
		/* can possibly allow ids for options/selected buckets */
		this._control_options = Object.extend({
			remove_event: 'dblclick',
			add_event: 'dblclick',
			options_title: null,
			selected_title: null,
			onInit: Prototype.emptyFunction,
			onOptionChange: Prototype.EmptyFunction,
			onOptionAdd: Prototype.emptyFunction,
			onOptionRemove: Prototype.emptyFunction,
			onSelectedChange: Prototype.emptyFunction,
			onSelectedAdd: Prototype.emptyFunction,
			onSelectedRemove: Prototype.emptyFunction,
			max_selected: 0
		}, options || {});
		
		if( this._selection_element	){
			this._selection_element.hide();
			this._id = this._selection_element.identify();
			
			var container = new Element('div', {'class': 'image_selection_bucket'});
			
			var otitle = this._control_options.options_title;
			if( otitle ){
				otitle = new Element('div', {'class': 'options_title'}).update(otitle);
			}
			this._options = new Element('div', {
				'id': this._id + '_options', 
				'class': 'image_options'
			});
			var stitle = this._control_options.selected_title;
			if( stitle ){
				stitle = new Element('div', {'class': 'selected_title'}).update(stitle);
			}
			this._selected = new Element('div', {
				'id': this._id + '_selected',
				'class': 'image_selected'
			});
			
			if( stitle ){
				container.insert(stitle);
			}
			container.insert(this._selected);
			if( otitle ){
				container.insert(otitle);
			}
			container.insert(this._options);
			this._selection_element.insert({after: container});
			
			$A(this._selection_element.options).each( function(o){
				this._addItem( o.value, o.text, false);
				if( o.selected ){
					this._addItem( o.value, o.text, true);
				}
				this._options_by_value.set( o.value, o);
			}.bind(this));
			Droppables.add(this._selected, {
				containment: this._id + '_options', 
				onDrop: this.select.bind(this)
			});
			this._fireCallback('onInit', this._selected_container);
		}
	},
	getID: function(){
		return this._id;
	},
	getSelectedContainer: function(){
		return this._selected;
	},
	getSelectedImages: function(){
		return this._selected_images;
	},
	getOptionContainer: function(){
		return this._options;
	},
	getOptionsImages: function(){
		return this._options_images;
	},
	select: function(elm){
		var val = elm.identify().split('_').last();
		if( val ){
			if(this._control_options['max_selected'] == 0 || this._currently_selected < this._control_options['max_selected']) {
				var o = this._options_by_value.get(val);
				try {
					if (!o.selected) {
						o.selected = true;
						var img = this._addImage(val, elm.src, true);
						this._currently_selected++;
						this._fireCallback('onSelectedAdd', img, val, this._selected);
						this._fireCallback('onSelectedChange', img, val, this._selected);
					}
				} 
				catch (e) {
				}
			}
		}
	},
	deselect: function(elm){
		var val = elm.identify().split('_').last();
		if( val ){
			var o = this._options_by_value.get(val);
			try{
				if( o.selected ){
					o.selected = false;
					elm = this._removeImage( elm, true);
					this._currently_selected--;
					this._fireCallback('onSelectedRemove', elm, val, this._selected);
					this._fireCallback('onSelectedChange', elm, val, this._selected);
				}
			}catch(e){}
		}
	},
	addElement: function( val, uri, selected){
		if (!this._options_by_value.get(val)) {
			if( Object.isUndefined(selected)){
				selected = false;
			}
			var o = this._addOption(val, uri, selected);
			var img = this._addImage(val, uri, selected);
			this._options_by_value.set(o.value, o);
			this._fireCallback('onOptionAdd', img, val, this._selected);
			this._fireCallback('onOptionChange', img, val, this._selected);
		}
	},
	removeElement: function(elm){
		elm = $(elm);
		var id = elm.identify().split('_').last();
		this.removeElementById(id, elm);
	},
	removeElementById: function(id, elm){
		var selm = this._getSelectedImageFromValue(id);
		if( selm ){
			this.deselect(selm);
		}
		if( !elm ){
			elm = this._getOptionImageFromValue(id);
		}
		this._removeOption( elm, id);
	},
	_getSelectedImageFromValue: function(v){
		return this._getImageFromValue( v, this._selected_images);
	},
	_getOptionImageFromValue: function(v){
		return this._getImageFromValue( v, this._options_images);
	},
	_getImageFromValue: function( v, arr){
		var l = arr.length;
		for( var i=0; i<l; i++){
			if( arr[i].identify().split('_').last() == v){
				return arr[i];
			}
		}
		return null;
	},
	_removeOption: function( elm, val){
		if( !val ){
			val = elm.identify().split('_').last();
		}
		try{
			var o = this._options_by_value.get(val);
			//console.log('about to remove option');
			o.remove();
			//console.log('removed option!');
			elm = this._removeImage(elm, false);
			//console.log('here! ' + this._options);
			this._fireCallback('onOptionRemove', elm, val, this._options);
			this._fireCallback('onOptionChange', elm, val, this._options);
			//console.log('fired callbacks');
		}catch(ex){
			//console.log(ex);
		};
	},
	_addOption: function( val, uri, selected){
		if( val && uri){
			var opt = new Element('option', {value: val}).update(uri);
			opt.selected = selected;
			this._selection_element.insert({bottom: opt});
			return opt;
		}else{
			return null;
		}
	},
	_removeImage: function(elm, selected){
		var l, i, arr, evt, obs;
		if( selected ){
			this._selected_images = this._selected_images.without(elm);
			evt = this._control_options.remove_event;
			obs = '_remove_observer';
		}else{
			this._options_images = this._options_images.without(elm);
			arr = this._options_images;
			evt = this._control_options.add_event;
			obs = '_add_observer';
		}
		try{
			elm.stopObserving( evt, elm[obs]);
			return elm.remove();
		}catch(ex){
			return null;
		};
	},
	_addImage: function( val, uri, selected){
		if (uri) {
			var bucket = this._getBucket(selected);
			var img = new Element('img', {
				src: uri,
				id: bucket.identify() + '_' + val
			});
			bucket.insert({
				bottom: img
			});
			if( !selected ){
				new Draggable( img, {
					revert: true,
					ghosting: false,
					onStart: function(e){},
					onEnd: function(e){},
					scroll: window
				});
				img._add_observer = this._selectEventWrapper.bind(this);
				img.observe( this._control_options.add_event, img._add_observer);
				this._options_images.push(img);
			}else{
				var o = this._deselectEventWrapper.bind(this);
				img._remove_observer = o;
				img.observe( this._control_options.remove_event, o);
				this._selected_images.push(img);
			}
			return img;
		}else{
			return null;
		}
	},
	_getBucket: function(selected){
		return selected?this._selected:this._options;
	},
	_deselectEventWrapper: function(evt){
		this.deselect(evt.element());
	},
	_selectEventWrapper: function(evt){
		this.select(evt.element());
	},
	_fireCallback: function(){
		var arguments = $A(arguments);
		if( arguments.length ){
			var f = arguments.shift();
			if( Object.isFunction( this._control_options[f])){
				this._control_options[f].apply( null, arguments);
			}
		}
	}
});

PSWrap.WindowScrollManager = {
	effect: null,
	scrollToElement: function( e ){
		var elm = $(e);
		if( elm ){
			var options = Object.extend({
				offset_x: 0,
				offset_y: 0
			}, arguments[1] || {});
			var p = elm.cumulativeOffset();
			options.x = p[0] + options.offset_x;
			options.y = p[1] + options.offset_y;
			PSWrap.WindowScrollManager.cancelScroll();
			PSWrap.WindowScrollManager.scrollTo( options );
		}
	},
	scrollTo: function( options){
		PSWrap.WindowScrollManager.cancelScroll();
		PSWrap.WindowScrollManager.effect = new Effect.Scroll(window, options);
	},
	cancelScroll: function(){
		var e = PSWrap.WindowScrollManager.effect;
		if( e ){
			e.cancel();
			e = null;
		}
	}
};